Skip to content

v1.81.0.0 feat: Aside is the browser gstack drives first; every browsing skill, the PDF/diagram renderer, and web research; the bundled browser stays the automatic fallback - #2810

Merged
garrytan merged 28 commits into
mainfrom
consolidate-browser-skills-into-aside
Sep 6, 2026

Conversation

@garrytan

@garrytan garrytan commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Community PR #2804 by @time-attack, merged with main's v1.80.0.0, re-versioned to v1.81.0.0, and hardened by the pre-landing review. Every commit on the branch is covered below; the release entry in CHANGELOG.md is the user-facing story.

Aside-first browser driver (the PR, 16 commits by @time-attack)

  • scripts/resolvers/aside.ts: the Aside browser-driver contract ({{ASIDE_SETUP}}), cookbook ({{ASIDE_COOKBOOK}}), research block ({{ASIDE_RESEARCH}}), written from live probes against Aside CLI 1.26; {{BROWSE_FALLBACK}} maps every Aside step onto $B so nothing regresses without Aside.
  • lib/aside-render.ts + bin/gstack-render.ts: local-HTML rendering through Aside first, gstack's bundled browser otherwise (ENGINE=aside|browse); make-pdf, /diagram, design previews and /office-hours sketches use it.
  • /qa, /qa-only, /design-review, /design-consultation, /benchmark, /canary, /land-and-deploy, /devex-review, /scrape, /browse drive Aside first; planning/review/security/investigate skills research through Aside's agent before WebSearch.
  • setup + gen-skill-docs prune renders of skills that no longer exist; lib/claude-bin.ts and lib/error-handling.ts become canonical.
  • Tests, registries, budgets, goldens, docs, CHANGELOG reconciled for Aside-first.

Merge with main's v1.80.0.0 (1 merge commit)

  • Three textual conflicts (CHANGELOG, TODOS, PROJECT_STRUCTURE) plus the invisible one: both sides claimed 1.80.0.0, so VERSION/package.json/digest auto-merged silently; queue-advanced to 1.81.0.0 with the size-budget baseline renamed to match.
  • Two semantic conflicts hidden in the clean setup auto-merge resolved in the merge itself: the prune honors main's fix(relink): don't clobber foreign skills that share a gstack name #2119 ownership gate; the browser hint and bootstrap summary honor _PW_FAIL_REASON and Aside presence.

Pre-landing review fixes (1 commit) — see "Pre-Landing Review" below

Coverage tests (1 commit), CHANGELOG/TODOS (2 commits), docs sync (1 commit)

Test Coverage

Audit ran on the merged branch before the fix wave (report-only): 29/44 paths tested (66%), 15 gaps. The fix and test commits then closed the named gaps with hermetic fake-executable tests; the free suite grew from 8,834 to 8,972 tests across 537 files (all green, zero flaky retries, evidence-ledgered).

Coverage diagram (pre-fix-wave audit)
CODE PATHS                                                                USER FLOWS
[+] lib/aside-render.ts                                                   [+] Render via bin/gstack-render.ts
  ├── [★   TESTED] probeAside (3 outcomes; skip-gate only, never asserted)  ├── [★   TESTED] Aside running (Mac) — aside-render.test:144 live/skipIf
  ├── [★★★ TESTED] lengthToInches/paperInches — aside-render.test:22-38     ├── [★★  TESTED] browse fallback — aside-render.test:233 live; [→E2E] diagram-triplet
  ├── [★★★ TESTED] buildRenderScript — :41-93                               ├── [GAP]        neither browser → NEEDS_ASIDE first line + exit 1
  ├── [★   TESTED] serveDir 200 only (live); 403 traversal / 404 never     ├── [GAP]        file outside --serve-root → ERROR, exit 1
  ├── [GAP]        runProc spawn-ENOENT / timeout kill                      └── [GAP]        page console.error → PAGE_ERRORS=[...] line
  ├── [GAP]        asideRender preconditions: missing file / outside root [+] make-pdf printing
  ├── [★   TESTED] asideRender stdout contract (live-only, Mac)             ├── [★   TESTED] print via Aside / fallback — test/e2e/*-gate (self-skip)
  ├── [★★★ TESTED] resolveBrowseBin + pdf/screenshot builders — :148-220    ├── [★★★ TESTED] no browser → BrowserUnavailableError — asideClient.test:123
  ├── [★★★ TESTED] renderWithBrowse bin=null → NO_BROWSER — :222            └── [GAP]        `pdf generate` exits 4 end-to-end (cli.ts mapping)
  ├── [★★  TESTED] renderWithBrowse happy + late readiness — :231 live    [+] ./setup
  ├── [GAP]        renderWithBrowse errors: no tabId, step fail, missing     ├── [★★★ TESTED] Linux, no Aside → fallback hint — setup-browser-hint.test:93
  │                artifact, until() timeout, finally closetab/rm           ├── [★★★ TESTED] macOS with Aside → Aside primary — :80
  └── [GAP]        pickEngine order + cache / render() dispatch             ├── [★★★ TESTED] failed Chromium bootstrap ×Aside present/absent — :85,:98,:109,:119
[+] bin/gstack-render.ts                                                    ├── [★★★ TESTED] skill removed → render+link pruned, user dir kept — setup-prune:94
  ├── [GAP]        argv parser + 13 usage-error branches                    └── [GAP]        frontmatter name ≠ dir survives prune (latent defect)
  └── [GAP]        output contract: ENGINE=/OK/EVAL/PAGE_ERRORS, exit codes [+] gen-skill-docs
[+] make-pdf/src                                                             └── [★★  TESTED] stale gstack-* pruned, sidecar kept — gen-skill-docs-prune-stale:17
  ├── [★★★ TESTED] asideClient pdfStepOptions/renderPdf/exit-4 class — asideClient.test:17-151
  ├── [GAP]        cli.ts error→exit mapping (BrowserUnavailable 4 / 2 / ENOENT 1)
  ├── [★   TESTED] orchestrator.generate wiring — test/e2e/*-gate (skip without dist/pdf + browser)
  ├── [GAP]        setup.ts runSetup (no engine → exit 4; smoke eval ≠ browser-ok → exit 4)
  ├── [★★★ TESTED] bundleRunner/runScript — diagram-prepass.test:441-530
  ├── [★★★ TESTED] renderFenceSlots/rasterize/downscale batch — coverage-gaps.test:55-150, diagram-prepass.test:407-436
  └── [★★★ TESTED] pdftotext isExecutable directory guard — pdftotext.test
[+] scripts/
  ├── [★★  TESTED] gen-skill-docs stale prune — gen-skill-docs-prune-stale:17 (no --dry-run / multi-host case)
  ├── [★★★ TESTED] resolvers/aside ASIDE_SETUP + COOKBOOK pins — aside-driver.test:31-115 [→EVAL]
  ├── [GAP] [→EVAL] resolvers/aside ASIDE_RESEARCH (embedded in 8 templates; no free pin)
  ├── [★★★ TESTED] resolvers/browse BROWSE_FALLBACK + 10-skill tripwire — aside-driver.test:117-189 [→EVAL]
  └── [★★★ TESTED] third-party-actions probe lifted byte-identical — third-party-actions.test
[+] setup (bash)
  ├── [★★★ TESTED] _prune_stale_generated — setup-prune-stale-generated.test:80-137
  ├── [★★★ TESTED] _browser_hint + Aside-aware Chromium summary — setup-browser-hint.test:75-135
  └── [GAP]        NEEDS_BUILD source list (make-pdf/src, design/src, lib/aside-render.ts newer than binary)
[+] lib/claude-bin.ts, lib/error-handling.ts (moved) — [★★★ TESTED] via browse/test/claude-bin + error-handling through re-exports

LLM integration: [GAP] [→EVAL] {{ASIDE_RESEARCH}} prompt block — behavior needs eval; skill-e2e-aside (periodic, self-skips without Aside) covers ASIDE_SETUP/COOKBOOK

COVERAGE: 29/44 paths tested (66%)  |  Code paths: 20/30 (67%)  |  User flows: 9/14 (64%)
QUALITY: ★★★:19 ★★:4 ★:6  |  GAPS: 15 (0 E2E, 1 eval)

Every [GAP] above except the two [→EVAL] prompt-block items now has a hermetic test (fake aside/browse executables, anchor-sliced setup blocks, spawned CLI); the two eval items are covered by the paid gate run below.

Tests: 618 → 622 test files (+4 new: test/gstack-render-cli.test.ts, test/setup-needs-build.test.ts, make-pdf/test/cli-exit-codes.test.ts, make-pdf/test/setup-smoke.test.ts); 5 existing test files extended (test/aside-render.test.ts (44 new cases), test/aside-driver.test.ts (11 new pins), test/setup-prune-stale-generated.test.ts, test/setup-browser-hint.test.ts, test/skill-parser.test.ts, test/gen-skill-docs-prune-stale.test.ts).

Pre-Landing Review

Checklist pass + 7 specialists (testing, maintainability, security, performance, data-migration, api-contract, design) + red team + Claude adversarial subagent + Codex adversarial (contained run; this sandbox lacks bubblewrap so Codex's own sandbox could not execute commands — it ran read-only in a throwaway worktree with credentials scrubbed). 55 specialist findings (4 critical), 15 Claude-adversarial, 11 Codex-adversarial, 5 red-team. Applied in commit 444f8fef:

  • setup prune (CRITICAL ×2, data-migration + security): the generator already pruned the render tree before setup ran, so the host-dir cleanup never fired; the gstack-*/ glob matched symlinks and rm -rf on the slash-terminated name empties the link's TARGET. Now scans host dirs too, skips render-tree symlinks, removes a host symlink only when it resolves into gstack, cleans bannered real dirs via _cleanup_weak_dir, recognizes frontmatter-renamed skills (externalSkillName), logs through log; the always-run codex render passes every host dir.
  • Loopback render server (CRITICAL testing + security + Codex P1): lexical containment followed symlinks and served the whole tree to any local process. Now: per-render secret path segment (404 without it), real-path containment (symlink escape 403), malformed encoding 400, directories 404.
  • Stdout trust (security + red team): inline eval results were raw multi-line page text on the same stream as ASIDE_DIR=/sentinel; a page could redirect the artifact copy. Evals are one base64 line each; the last ASIDE_DIR wins; gstack-render fences EVAL/PAGE_ERRORS as ═══ UNTRUSTED WEB CONTENT ═══.
  • runProc (Claude adversarial + Codex P2): SIGTERM-only with unbounded pipe waits; now SIGKILL escalation, bounded reads, every timer cleared (an uncleared timer kept gstack-render alive up to 140s after printing OK — caught by the new tests).
  • Engine choice (red team + Codex P2): render() retries on gstack's own browser when Aside's CLI cannot start or its private _sendToTarget bridge is gone (never on a page error or a timeout); the CLI and make-pdf report the engine that actually rendered.
  • Skill prose: .local removed from the LOCAL rule (mDNS = other LAN machines); same-origin filters compare parsed origins (https://example.com.attacker.tld passed startsWith); link status HEAD-checked only on LOCAL targets (on a real site every request carried the user's cookies); probe deadline via perl alarm on stock macOS; GSTACK_SKIP_ASIDE=1 honored by probe, setup and renderer; every aside exec goes through the receipted _aside_exec prelude ({{ASIDE_EXEC_PRELUDE}}), including nine template blocks that called it bare; design sketch and diagram staging use private directories.
  • Generator prune (Codex P0): a host whose generation threw had a partial rendered set, so the prune would delete every valid render it never reached; now skipped for failed hosts, and only bannered gstack-* dirs are ever removed.
  • setup rebuild check (Codex P1 + data-migration): only the browse binary was existence-checked and lib/ (the moved canonical sources) was not in the staleness set; _EXE was dead.
  • CLI contract (api-contract): --help exit 0, non-numeric flags rejected, --wait-timeout documented, --quiet doc matched to behavior; setup.ts failure names the remedy; stale references in find-browse.ts/AGENTS.md/BROWSER.md/SLOP_SCAN.md fixed; duplicated footer/tmp-dir/magic numbers consolidated.

Deferred to TODOS.md (with rationale): make-pdf CSP for user documents rendered in the real browser profile (P1), diagram pre-pass memory cap (P1), fallback render vs the daemon's cookie-import JS lock (P2), carving the Aside contract into a shared section (P2), $B js envelope (P2), perf/DRY items (P3), gen-skill-docs --dry-run write leak (P3), non-Claude host loops under the marker rule (P2), CHANGELOG-heading == VERSION pin (P3).

Kept as-is, disclosed: the Aside path itself cannot execute on Linux (macOS-only; the contributor's macOS evidence and the self-skipping test/skill-e2e-aside.test.ts lane cover it; a read-only probe on the maintainer's Mac confirmed Aside CLI 1.26.902.1732 answers aside repl in 220ms); codex review --base could not run here (CLI rejects -s; its sandbox needs bubblewrap), so the Codex structured gate is informational.

Design Review

Design specialist (lite) ran (SCOPE_FRONTEND=true from the diff scope): no user-visible UI changed (the only markup is a CDP print footer ported byte-for-byte). No findings. Codex design voice not run (see sandbox note above).

Eval Results

Gate tier (what CI runs per PR), sharded paid runner via bun run eval:bg:gate (gstack-detach, lock gstack-evals): 50/50 shards executed — 49 passed, 1 failed, 0 timed out, 0 never started; selection was all 178 E2E + 25 LLM-judge tests because test/helpers/hermetic-env.ts is a global touchfile. test/skill-llm-eval.test.ts (LLM judge) passed.

  • The one failure, test/skill-e2e-bws.test.ts, was the PR's own doing: its three cases slice the $B setup block out of browse/SKILL.md between ## SETUP and ## Core QA Patterns, both gone since Aside became the primary driver, so the agent received an empty block. Anchors moved to the fallback section's ### Find the $B binary in 2649a3b6; re-run alone: 7/7 pass.
  • 11 shards passed with every test self-skipped (lanes that need Aside, an iOS device, gbrain, or Codex/Gemini): they verified nothing here and are marked as such in the run log.
  • Not run here: codex review --base (its sandbox needs bubblewrap, unavailable in this environment); Codex adversarial ran in a contained worktree instead.

Scope Drift

Scope Check: CLEAN. Intent: land PR #2804 (Aside-first) against current main. Delivered: the PR plus the merge, the re-version, and review fixes that only tighten the PR's own surfaces (no new features). Two additions beyond the PR's scope are hardening the review demanded: the receipted aside exec wrapper and the loopback server's secret path.

Plan Completion

Plan file audited by a fresh subagent: 45 items — 37 DONE, 2 CHANGED, 0 NOT DONE, 2 UNVERIFIABLE (session-performed process steps), 4 deferred by design (push/re-target, branch rename, paid evals, Aside-present E2E).

  • Branch = PR tip (16 commits, authorship preserved) + one merge of origin/main + follow-up commits
  • Re-versioned to v1.81.0.0 everywhere (VERSION, package.json 1.81.0, digest, CHANGELOG top entry dated 2026-09-06 with Contributed by @time-attack., baseline renamed); slot re-checked free before push
  • Three textual conflicts resolved; README + TESTING_INTERNALS reconciled with the best-effort Chromium bootstrap
  • setup semantic fixes with tests; gen:skill-docs --host all clean; full bun run build exit 0
  • [~] CHANGED: merge-commit title shortened (fix clause moved to body); origin/main blame worktree not needed (no failures to blame)
  • UNVERIFIABLE by diff: Aside probe on the user's Mac (done in session, recorded above); GIT_EDITOR process step

Verification Results

Skipped: no dev server (CLI + skill templates; nothing to browse). /qa does not apply.

TODOS

No TODO items completed in this PR (the PR itself marks the "QA logged-in-evidence path via Aside (Phase 2)" item as landed). Filed: 2 follow-ups from the plan reviews and 8 from the pre-landing review (see TODOS.md, "Aside-first follow-ups" and NEXT PRIORITY).

Documentation

Post-ship doc sync for the v1.81.0.0 review fixes (docs commit 97fc8808). No SKILL.md/.tmpl touched; CHANGELOG, VERSION and TODOS untouched by the sync (the 1.81.0.0 entry already covers the fix wave).

  • BROWSER.md: gstack-render output contract (ENGINE= names the engine that actually rendered; retry on gstack's own browser only when Aside's CLI cannot start or its CDP bridge is missing), EVAL/PAGE_ERRORS fenced as untrusted web content, --wait-timeout, --help exit 0, numeric-flag guards; the loopback server's per-render secret path and real-path containment; LOCAL rule without *.local; LOCAL-only HEAD checks; receipted _aside_exec; 30s probe bound; GSTACK_SKIP_ASIDE=1; audit-trail and CI bullets corrected.
  • ARCHITECTURE.md: same render facts; {{ASIDE_EXEC_PRELUDE}} row; research paragraph notes the receipt.
  • CONTRIBUTING.md: render guidance points at render() with the engine-specific halves; exported test seams (pickEngine(fresh, deps), serveDir, SAFE_TMP_DIR, PAGE_NUMBER_FOOTER); new Tier-1 test bullets; rebuild rule = three binaries + lib/; the prune's ownership gate; GSTACK_SKIP_ASIDE.
  • CLAUDE.md: ownership gate covers the retired-skill prune and pins its test.
  • README.md: troubleshooting adds GSTACK_SKIP_ASIDE=1 and the ENGINE= first line.
  • docs/TESTING_INTERNALS.md, docs/PROJECT_STRUCTURE.md: E2E gating corrected; new anchor-sliced setup tests and the CLI test listed.

Codex documentation review: 9 gaps, each verified against code, all fixed in the same commit. Documentation debt (reference-only, no how-to yet): GSTACK_SKIP_ASIDE=1, gstack-render flags, the exported test seams, {{ASIDE_EXEC_PRELUDE}}.

Test plan

  • Free suite via the strict runner: 8,972 tests across 537 files, 0 fail, 0 flaky retries (evidence ledger FRESH at commit time)
  • bun run gen:skill-docs --host all && git diff --exit-code clean; ship goldens re-rendered
  • Full bun run build (all binaries) exit 0
  • Targeted gates before each commit: context-budget ratchet, size budget, digest/llms freshness, setup harnesses
  • Gate-tier paid evals: 50/50 shards, 49 pass + the one failing gate fixed and re-run green

🤖 Generated with Claude Code


Open workspace in Conductor

time-attack and others added 24 commits September 5, 2026 16:40
… resolvers

{{ASIDE_SETUP}} (readiness probe + ten rules for driving the user's real browser), {{ASIDE_COOKBOOK}} (script shapes verified live against Aside CLI 1.26: one flow per aside repl script, CDP console hook before navigation, evidence lines, session-directory artifact handoff, GSTACK_STEP_OK sentinel), {{ASIDE_RESEARCH}} (research through aside exec, WebSearch when Aside is absent, knowledge otherwise) and {{BROWSE_FALLBACK}} (the fifteen-row Aside-step to $B-command table plus the rules that differ, so every browsing skill keeps working on gstack's own headless browser). test/aside-driver.test.ts pins the sentences and asserts every browsing skill carries the Aside block followed by the fallback; test/helpers/aside-available.ts is the shared live-Aside probe.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…r as fallback

lib/aside-render.ts serves the HTML's directory on loopback (Aside refuses file:// URLs), opens it with waitUntil load, prints through CDP Page.printToPDF so tagged output, outlines, header/footer templates and page numbers survive, emulates device metrics for sized screenshots, and writes in-page evaluations to files; when Aside is absent it runs the same spec through the browse daemon (newtab, load, js, pdf, screenshot, closetab) and reports ENGINE=aside|browse. bin/gstack-render.ts is the CLI skill templates call. lib/claude-bin.ts and lib/error-handling.ts become the canonical copies (browse/src re-exports them).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ehind the fallback

Contract, cookbook, mode choice (aside repl by default, aside exec for reading), report format, the fallback section, and the full command reference carved on demand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
QA_METHODOLOGY runs every phase as Aside scripts (orient, explore, document, re-test, mobile viewport via CDP emulation, links via HEAD fetch); the authenticate phase is 'you are already signed in'; a 13th rule requires consent before mutating actions on non-local targets; the fallback section translates each step onto $B. The qa E2E tests run on whichever engine is present.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… plan-design-review, design-html drive Aside

Design-system extraction is one script printing FONTS/COLORS/HEADINGS/TOUCH_TARGETS/NAV; competitor research confirms the exact URLs before opening them in the real browser and runs on the bundled browser when Aside is absent; design-html's viewport screenshots, sketches and comparison boards render through gstack-render.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…view drive Aside

One aside repl script per page prints NAV/PAINT/LCP/RESOURCES/SCRIPTS/CSS/SUMMARY (benchmark), CONSOLE_ERRORS/NAV/TEXT + screenshot (canary, re-run every 60s), and the post-deploy check reads responseStatus from the navigation entry; each carries the $B fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…visible browser stays the fallback

The readiness probe is lifted from {{ASIDE_SETUP}} at gen time (byte-identity pinned) and rule 3 points at browse/SKILL.md for how to drive; the consent question offers Aside first and gstack's own visible browser (handoff/resume for sign-in) as the fallback, as v1.72 framed it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ls runtime rides the fallback

Look-then-extract scripts build the JSON inside the page and print it between JSON_START/JSON_END; aside exec for fuzzy intents; on the $B fallback the browser-skills match/prototype flow and /skillify apply as before.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…herwise

asideClient.ts replaces the direct $B client with one render() call per PDF (the exact option mapping the browse pdf command had: paper, margins, header/footer/page numbers, tagged, outline, printBackground, preferCSSPageSize, Paged.js wait); the diagram pre-pass, oversized-image downscale and DOCX rasters each run as one render script with per-fence try/catch; exit 4 now means no browser is available and names both remedies; $P setup reports which engine it found. The e2e gates run on whichever engine is present, so the Linux lane exercises the fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
SVG, PNG and excalidraw from one invocation over the content-addressed bundle staged under /tmp/gstack-render; every diagram type gets an excalidraw export; gstack-render picks the engine and prints ENGINE=; the diagram E2E gates on either engine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The planning, review, design, security and investigate skills research through {{ASIDE_RESEARCH}}; WebSearch stays in allowed-tools as the fallback; testing.ts's bootstrap step follows; skeleton ceilings ratcheted for the research block.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
setup gains _prune_stale_generated for every host tree and the doc generator removes gstack-* output dirs it did not write, so a skill removed from the source tree can never linger in an install.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he $B fallback

Touchfiles + E2E tiers gain the Aside keys, coverage matrix and eval baselines updated, size budget re-baselined to parity-baseline-v1.80.0.0.json (the contract plus fallback ride in every browsing skill), parity ceilings ratcheted with measured values, LLM-judge prompts and the E2E fixtures speak Aside-first, browse-fallback.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
README, BROWSER.md, docs/, CONTRIBUTING, CLAUDE.md, ARCHITECTURE, AGENTS.md, TODOS and the root router describe the one product story: Aside is the browser gstack drives first; the bundled headless browser is the automatic fallback (Linux, Windows, app closed) where cookie import, GStack Browser, pair-agent and browser-skills still apply.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s, context-budget fixture

bun run gen:skill-docs over the templates; goldens re-rendered; context-budget ceilings recaptured.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ser is the fallback

MINOR: new capability across ten skills, the renderer and research; nothing removed. CHANGELOG release summary + itemized changes; VERSION 1.80.0.0; package.json 1.80.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ide; queue-advance release to v1.81.0.0

Both sides claimed v1.80.0.0, so VERSION, package.json and the agents digest
auto-merged without a conflict; the release is re-versioned to the next free
MINOR slot (bin/gstack-next-version), the size-budget baseline renamed to
match, and the CHANGELOG carries the Aside-first entry above main's.

Two semantic conflicts hidden in the clean setup auto-merge are resolved here:
- _prune_stale_generated deleted a REAL host directory on banner-only proof;
  main's #2119 gate makes that weak proof file-scoped, so real dirs now go
  through _cleanup_weak_dir (SKILL.md, marker and our links only).
- _browser_hint and the Chromium bootstrap summary now consult _PW_FAIL_REASON
  and Aside presence, so they never promise a bundled browser that cannot
  launch and never tell an Aside user their browser skills are gone.
README, TESTING_INTERNALS and TODOS wording reconciled with the merged tree.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…pin follow-ups

Two follow-ups from the /plan-ceo-review + /plan-eng-review pass on merging
PR #2804 with main's v1.80.0.0 ownership gate: bring the Codex/Factory/
OpenCode/Cursor/Kiro copy loops and the stale-render prune under the
.gstack-owned marker rule, and a free test pinning that the CHANGELOG top
heading equals VERSION (the collision that git cannot see).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Review army + adversarial passes (Claude and Codex) on the merged branch:

setup
- _prune_stale_generated scans the host dirs too (the generator already
  removed the render before setup ran, so the host branch was dead), skips
  symlinks in the render tree (rm -rf on a slash-terminated link empties its
  target), removes a host symlink only when it resolves into gstack, cleans a
  bannered real dir through _cleanup_weak_dir, recognizes frontmatter-renamed
  skills, and logs through log. The always-run codex render passes every host
  dir that may link to it.
- NEEDS_BUILD checks all three binaries (with $_EXE) and lib/ sources; the
  browser hint and the bootstrap summary honor GSTACK_SKIP_ASIDE, treat a
  requested skip as a request, and derive one skill list.

lib/aside-render.ts + bin/gstack-render.ts
- The loopback server carries a per-render secret path, checks containment on
  the real path (symlink escapes are 403), and rejects malformed encoding.
- Inline eval results are one base64 line, so page text cannot forge
  ASIDE_DIR= or the sentinel; the last ASIDE_DIR wins.
- runProc escalates SIGTERM to SIGKILL, bounds every wait, and clears every
  timer (an uncleared one kept gstack-render alive after printing OK).
- renderTmpDir refuses a shared /tmp name owned by someone else; the work dir
  and server are created inside try; goto's budget follows the render budget.
- probeAside classifies a present-but-failing CLI as ASIDE_NOT_RUNNING like
  the skills' bash probe; render() retries on gstack's own browser when Aside
  could not start or its private CDP bridge is gone (never on a page error
  or a timeout of a running script); the CLI reports the engine that actually
  rendered, exits 0 on --help, rejects non-numeric flags, documents
  --wait-timeout, fences EVAL/PAGE_ERRORS as untrusted content, and names the
  daemon's cookie-import JS lock remedy.
- The browse path passes --scale only when asked (a scale change rebuilds
  the daemon context) and restores the viewport after a sized screenshot.

resolvers / templates
- The bash probe honors GSTACK_SKIP_ASIDE and has a perl deadline on stock
  macOS; .local is no longer LOCAL (mDNS); same-origin filters compare parsed
  origins; link status is HEAD-checked only on LOCAL targets; every
  aside exec goes through the receipted _aside_exec prelude
  ({{ASIDE_EXEC_PRELUDE}}), including nine template blocks that called it
  bare; the design sketch and diagram staging use private directories.
- The generator prunes only bannered renders and never a host whose
  generation failed.

Docs, stale comments and dead code cleaned; goldens re-rendered; tests
updated and added for every behavior above.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… codes, and prose $B spans

New free tests from the ship coverage audit: test/gstack-render-cli.test.ts
(argv guards, --help, output contract with a fake daemon, failure and
serve-root paths, no-browser case, prompt exit), test/setup-needs-build.test.ts
(every binary and source set flips NEEDS_BUILD, Windows suffixes),
make-pdf/test/cli-exit-codes.test.ts and setup-smoke.test.ts (error to exit
code mapping, runSetup stages, renderPdf's engine), and prose-span cases for
extractBrowseCommands in test/skill-parser.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
BROWSER.md, ARCHITECTURE.md, CONTRIBUTING.md, README.md, CLAUDE.md,
docs/TESTING_INTERNALS.md and docs/PROJECT_STRUCTURE.md now describe the
shipped renderer and setup: the loopback render server's per-render secret
path and real-path containment, ENGINE= naming the engine that actually
rendered (mid-run retry on gstack's own browser), EVAL/PAGE_ERRORS fenced as
untrusted content, --wait-timeout and the CLI's argv guards, the receipted
_aside_exec prelude ({{ASIDE_EXEC_PRELUDE}} in the placeholder table), the
LOCAL host rule without .local, LOCAL-only HEAD checks in the links script,
GSTACK_SKIP_ASIDE across probe/renderer/setup, the ownership-gated
retired-skill prune, the widened NEEDS_BUILD check, and the new free tests
(gstack-render-cli, setup-prune-stale-generated, setup-browser-hint,
setup-needs-build, make-pdf cli-exit-codes and setup-smoke).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…allback section

browse/SKILL.md no longer has '## SETUP' / '## Core QA Patterns' (Aside is the
primary driver; the $B block moved under 'Browser fallback'), so the gate test
sliced an empty block and handed the agent nothing to run. Anchor on
'### Find the `$B` binary' up to the next heading. 7/7 pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@trunk-io

trunk-io Bot commented Sep 6, 2026

Copy link
Copy Markdown

😎 This pull request was merged.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

E2E Evals: ✅ PASS

92/92 tests passed | $13.10 total cost | reconcile exit: 0 | ⚠ 2 flaky pass(es) — recorded, not blocking

Shard Result Status Cost
e2e-browse/skill-e2e-bws 7/7 $0.41
e2e-coverage-audit/skill-e2e-coverage-audit 2/2 $0.19
e2e-cso/skill-e2e-cso 1/1 $0.48
e2e-deploy/skill-e2e-deploy 6/6 $1.24
e2e-design/skill-e2e-design 1/1 $0.13
e2e-docsync-spawned/skill-e2e-docsync-spawned 1/1 $0.15
e2e-hermetic/skill-e2e-hermetic-canary 2/2 $0.02
e2e-learnings/skill-e2e-learnings 1/1 $0.12
e2e-opus-47/skill-e2e-opus-47 6/6 $1.38
e2e-plan-tune/skill-e2e-plan-tune 1/1 $0.19
e2e-plan/skill-e2e-plan 7/7 $1.96
e2e-qa-workflow/skill-e2e-qa-workflow 3/3 $1.33
e2e-retro/skill-e2e-retro 1/1 $0.3
e2e-review-army/skill-e2e-review-army 5/5 $0.82
e2e-review-attribution/skill-e2e-review-attribution 3/3 ✅⚠ $0.3
e2e-review/skill-e2e-review 2/2 $0.24
e2e-session-intelligence/skill-e2e-session-intelligence 4/4 $0.31
e2e-ship-docsync/skill-e2e-ship-docsync 1/1 $0.37
e2e-skillify/skill-e2e-skillify 3/3 $1.25
e2e-third-party-actions/skill-e2e-third-party-actions 5/5 ✅⚠ $0.46
e2e-triage/skill-e2e-triage 1/1 $0.15
e2e-workflow/skill-e2e-workflow 4/4 $0.8
llm-judge/skill-llm-eval 25/25 $0.5
Fail-closed reconciliation
fice-hours-brain-writeback.test.ts
  slice 3  passed             39s  test/skill-e2e-plan-design-with-ui.test.ts
  slice 3  passed              0s  test/skill-e2e-plan-tune-cathedral.test.ts
  slice 3  passed            252s  test/skill-e2e-review-army.test.ts
  slice 3  passed            100s  test/skill-e2e-third-party-actions.test.ts
  slice 4  passed             90s  test/skill-e2e-bws.test.ts
  slice 4  passed             27s  test/skill-e2e-diagram.test.ts
  slice 4  passed              0s  test/skill-e2e-ios-swift-build.test.ts
  slice 4  passed              0s  test/skill-e2e-office-hours-phase4.test.ts
  slice 4  passed            182s  test/skill-e2e-plan-devex-finding-floor.test.ts
  slice 4  passed             39s  test/skill-e2e-plan-tune.test.ts
  slice 4  passed            439s  test/skill-e2e-review-attribution.test.ts
  slice 4  passed             31s  test/skill-e2e-triage.test.ts
  slice 5  passed              0s  test/skill-e2e-context-skills.test.ts
  slice 5  passed             52s  test/skill-e2e-docsync-spawned.test.ts
  slice 5  passed              0s  test/skill-e2e-ios.test.ts
  slice 5  passed              0s  test/skill-e2e-office-hours.test.ts
  slice 5  passed            233s  test/skill-e2e-plan-devex-plan-mode.test.ts
  slice 5  passed            292s  test/skill-e2e-plan.test.ts
  slice 5  passed             82s  test/skill-e2e-review.test.ts
  slice 5  passed            204s  test/skill-e2e-workflow.test.ts
  slice 6  passed             47s  test/skill-e2e-coverage-audit.test.ts
  slice 6  passed              0s  test/skill-e2e-first-task-scaffold.test.ts
  slice 6  passed             25s  test/skill-e2e-learnings.test.ts
  slice 6  passed             18s  test/skill-e2e-opus-47.test.ts
  slice 6  passed              0s  test/skill-e2e-plan-format.test.ts
  slice 6  passed              0s  test/skill-e2e-qa-bugs.test.ts
  slice 6  passed             55s  test/skill-e2e-session-intelligence.test.ts
  slice 6  passed              0s  test/skill-llm-eval-spec.test.ts
[test:paid] report: ⚠ 2 test(s) passed only on retry this run (recorded, not blocking):
  ⚠ tpa-absent-darwin (x2) — shards/skill-e2e-third-party-actions/1.81.0-HEAD-e2e-third-party-actions-2026-09-06-1520.json
  ⚠ /ship review dashboard via attribution (x2) — shards/skill-e2e-review-attribution/1.81.0-HEAD-e2e-review-attribution-2026-09-06-1527.json
[test:paid] report: ⚠ 14 shard(s) passed with EVERY test skipped — they verified nothing:
  ⚠ test/llm-judge-recommendation.test.ts (1 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-gbrain-roundtrip-local.test.ts (3 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-plan-prosons.test.ts (12 skipped — external service missing or tier mismatch)
  ⚠ test/skill-routing-e2e.test.ts (10 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-autoplan-dual-voice.test.ts (3 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-ios-device.test.ts (4 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-office-hours-brain-writeback.test.ts (3 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-plan-tune-cathedral.test.ts (5 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-office-hours-phase4.test.ts (3 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-context-skills.test.ts (9 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-office-hours.test.ts (6 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-first-task-scaffold.test.ts (1 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-plan-format.test.ts (12 skipped — external service missing or tier mismatch)
  ⚠ test/skill-e2e-qa-bugs.test.ts (5 skipped — external service missing or tier mismatch)
[test:paid] report: every planned shard accounted and passed

Sliced lane: diff-selected gate census via scripts/test-paid-shards.ts (planner → 6 executors → fail-closed report)

garrytan and others added 3 commits September 6, 2026 14:37
windows-free-tests: the gstack-render CLI tests drive a shebang fake browse
that CreateProcess cannot exec, and two NEEDS_BUILD cases assert an execute
bit and a bare-name miss that MSYS bash does not have (test -x ignores mode
bits and resolves design -> design.exe). Those describes and cases now
self-skip on win32; argument guards, --help, the no-browser case, and every
other rebuild-check case still run there.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… newtab retries once on a cold daemon

A process whose pipes have reached EOF is exiting, but runProc gave the exit
code only five seconds to arrive and then returned null, which run() reports
as a failed command. Under CI's six-shard load one such render failed with the
artifact already written. The SIGTERM/SIGKILL timers already bound the wait,
so the exit race now runs to the kill deadline.

The first CLI call auto-starts the browse daemon; on a cold start it can
answer 'Unable to connect' once while the server is still coming up. That
single case is retried after 1.5s; every other newtab failure is not.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…res name the render error

- Live fallback cases run 'goto about:blank' up to twice before asserting and
  skip (never fail) when the daemon cannot come up.
- expectOk() puts r.error and the browse transcript into the assertion so a
  failed render is diagnosable from the CI log.
- The argv-contract cases dump the fake's log on a miss.
- File default timeout is 30s: the subject is the CLI contract, not latency.
- Two cases pin the cold-daemon newtab retry and that other errors are not
  retried.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… renderer

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@garrytan
garrytan merged commit 0530392 into main Sep 6, 2026
20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants